package com.happe.foodeasy.helper;

import org.apache.commons.beanutils.BeanUtils;

import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.net.JarURLConnection;
import java.net.URL;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.Map;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

/**
 * This class is used to give help for class reflect
 * Created by wubin on 2016-05-10.
 */
public class ClassOperateHelper {
    /**
     * In one class, maybe has many fields, this method is used to retrieve the field that
     * has getXXX method or setXXX method, in other words, the target field must one property of
     * corresponding class, will be retrieved.
     *
     * @param targetClass the target class.
     * @return the fields what are the properties of corresponding class.
     */
    public static Field[] retrieveFieldsBelongProperty(Class targetClass) {
        Field[] fields = targetClass.getDeclaredFields();
        List<Field> result = new ArrayList<>();
        for (Field field : fields) {
            if (oneFiledIsProperty(targetClass, field)) {
                result.add(field);
            }
        }
        return result.toArray(new Field[result.size()]);
    }

    /*
    * This method is used to check one target field is the property of targetClass.
    * */
    private static boolean oneFiledIsProperty(Class targetClass, Field field) {
        try {
            BeanInfo beanInfo = Introspector.getBeanInfo(targetClass, Object.class);
            PropertyDescriptor[] descriptors = beanInfo.getPropertyDescriptors();
            for (PropertyDescriptor descriptor : descriptors) {
                if (descriptor.getDisplayName().equals(field.getName())) {
                    return true;
                }
            }

            return false;
        } catch (IntrospectionException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * This method is used to find all class in target package, but not include
     * sub package, just in current package path.
     *
     * @param packName the target package name that should be parse
     * @return the all classes in target package.
     */
    public static Class[] findClassInTargetPackage(String packName) {
        List<Class> result = new ArrayList<>();
        String packagePath = packName.replace(".", "/");
        Enumeration<URL> urls;
        try {
            urls = Thread.currentThread().getContextClassLoader().getResources(packagePath);
            while (urls.hasMoreElements()) {
                URL url = urls.nextElement();
                String protocol = url.getProtocol();
                if ("file".equals(protocol)) {
                    String filePath = URLDecoder.decode(url.getFile(), "UTF-8");
                    findClassInPackageByFile(packName, filePath, result);
                } else if ("jar".equals(protocol)) {
                    findClassInPackageByJar(url, packName, result);
                }
            }
            return result.toArray(new Class[result.size()]);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * check the targetClass whether is the byte array type.
     *
     * @param targetClass the target class that should be checked.
     * @return whether the targetClass is byte array type.
     */
    public static boolean checkTargetClassIsByteArray(Class targetClass) {
        return targetClass.isArray() && targetClass.getComponentType().getSimpleName().equals("byte");
    }

    public static boolean checkTargetClassIsBoolean(Class targetClass) {
        return targetClass.getSimpleName().equals("Boolean");
    }

    /**
     * This method is used to create target entity for target class.
     *
     * @param targetClass the class of target.
     * @param map         key is the property's name of target class, the value is the property's value.
     * @param <T>         the target class's type.
     * @return the target entity.
     */
    public static <T> T createTargetInstance(Class<T> targetClass, Map<String, Object> map) {
        try {
            T targetEntity = targetClass.newInstance();
            for (String key : map.keySet()) {
                BeanUtils.setProperty(targetEntity, key, map.get(key));
            }
            return targetEntity;
        } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
            throw new RuntimeException(e);
        }
    }

    @SuppressWarnings("all")
    private static void findClassInPackageByJar(URL url, String packName, List<Class> result) {
        JarFile jarFile;
        try {
            jarFile = ((JarURLConnection) url.openConnection()).getJarFile();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

        List<JarEntry> jarEntryList = new ArrayList<>();
        Enumeration<JarEntry> entries = jarFile.entries();
        String regex = String.format("%s/[a-zA-Z]+\\.class", packName.replace(".", "/"));
        while (entries.hasMoreElements()) {
            JarEntry oneEntry = entries.nextElement();
            String oneEntryName = oneEntry.getName();
            if (oneEntryName.matches(regex)) {
                try {

                    result.add(Class.forName(oneEntryName.replace("/", ".").substring(0, oneEntryName.lastIndexOf(".class"))));
                } catch (ClassNotFoundException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    private static void findClassInPackageByFile(String packName, String filePath, List<Class> result) {
        File dir = new File(filePath);
        if (!dir.exists() || !dir.isDirectory()) {
            throw new RuntimeException("所给包无法找到或不存在");
        }

        File[] dirFiles = dir.listFiles(new FileFilter() {
            @Override
            public boolean accept(File file) {
                return file.getName().endsWith("class");
            }
        });

        for (File file : dirFiles) {
            String className = file.getName().substring(0, file.getName().lastIndexOf(".class"));
            try {
                result.add(Class.forName(packName + "." + className));
            } catch (ClassNotFoundException e) {
                throw new RuntimeException(e);
            }
        }
    }

    /**
     * This class is used to retrieve inner class in targetClass.
     *
     * @param tClass               the target class that should be checked.
     * @param targetInnerClassName the name of inner class.
     * @param <T>                  the target class' type.
     * @return the
     */
    public static <T> Class retrieveTargetInnerClassInClass(Class<T> tClass, String targetInnerClassName) {
        Class<?>[] allInnerClass = tClass.getDeclaredClasses();
        for (Class<?> oneClass : allInnerClass) {
            if (oneClass.getSimpleName().equals(targetInnerClassName)) {
                return oneClass;
            }
        }

        return null;
    }
}
